﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace RemoteTestRunner.UnitTesting
{
    /// <summary>
    /// Test class level object for tracking individual asserts in test methods that have multiple asserts.
    /// </summary>
    public class MultiAssertTestClassHelper : IDisposable
    {
        #region Private Attributes

        // MSTest tests can run in parallel.  This class must be able to support that functionality.
        private const string MultiAssertFileSuffix = ".MultiAssertOutput.xml";

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="MultiAssertTestClassHelper"/> class.
        /// </summary>
        /// <param name="testClassName">The name of the test class.</param>
        /// <param name="testDirectory">The test directory.</param>
        /// <remarks>This version of the constructor will not download any configured data
        /// sources.  Use an overloaded version if you want all configured data sources
        /// to be downloaded prior to any unit tests being executed.</remarks>
        [Obsolete(
            "This constructor is no longer supported.  Please use an overridden version that includes TestContext.",
            true)]
        public MultiAssertTestClassHelper(string testClassName, string testDirectory)
        {
            TestClass = testClassName;
            OutputFile = testDirectory + "." + testClassName + MultiAssertFileSuffix;

            Functions = new List<TestMethodHelper>();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MultiAssertTestClassHelper"/> class.
        /// </summary>
        /// <param name="testClassName">The name of the test class.</param>
        /// <param name="testContext">A <see cref="TestContext"/> instance used to provide
        /// information about the unit tests.</param>
        /// <remarks>All configured data sources will be downloaded and cached per the active
        /// configuration settings prior to any tests being executed.</remarks>
        public MultiAssertTestClassHelper(string testClassName, TestContext testContext)
            : this(testClassName, testContext, true, AppDomain.CurrentDomain.SetupInformation.ConfigurationFile)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MultiAssertTestClassHelper"/> class.
        /// </summary>
        /// <param name="testClassName">The name of the test class.</param>
        /// <param name="testContext">A <see cref="TestContext"/> instance used to provide
        /// information about the unit tests.</param>
        /// <param name="downloadDataSources">Specifies whether any configured data sources should be downloaded
        /// prior to executing any unit tests.</param>
        /// <remarks>All configured data sources will be downloaded and cached per the active
        /// configuration settings prior to any tests being executed.</remarks>
        public MultiAssertTestClassHelper(string testClassName, TestContext testContext, bool downloadDataSources)
            : this(
                testClassName, testContext, downloadDataSources,
                AppDomain.CurrentDomain.SetupInformation.ConfigurationFile)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MultiAssertTestClassHelper"/> class.
        /// </summary>
        /// <param name="testClassName">The name of the test class.</param>
        /// <param name="testContext">A <see cref="TestContext"/> instance used to provide
        /// information about the unit tests.</param>
        /// <param name="downloadDataSources">Specifies whether any configured data sources should be downloaded
        /// prior to executing any unit tests.</param>
        /// <param name="configPath">The fully-qualified path of the configuration file containing the data source
        /// information to be used by the data-driven unit tests.</param>
        public MultiAssertTestClassHelper(string testClassName, TestContext testContext, bool downloadDataSources,
                                          string configPath)
        {
            if (string.IsNullOrEmpty(testClassName))
            {
                throw new ArgumentNullException("testClassName", "Please specify a valid class name.");
            }

            if (testContext == null)
            {
                throw new ArgumentNullException("testContext", "Please specify a valid TestContext.");
            }

            TestClass = testClassName;
            OutputFile = testContext.TestDir + "." + testClassName + MultiAssertFileSuffix;

            Functions = new List<TestMethodHelper>();

            // Download and cache any configured data files if desired
            if (downloadDataSources)
            {
                if (string.IsNullOrEmpty(configPath))
                {
                    throw new ArgumentNullException("configPath", "Please specify a valid configuration path.");
                }

                if (File.Exists(configPath))
                {
                    var dataSources = GetDataSources(configPath);

                    if ((dataSources != null) && (dataSources.Count > 0))
                    {
                        // Download each file
                        foreach (var dataSource in dataSources)
                        {
                            if (!string.IsNullOrEmpty(dataSource.DataSourceBaseUri))
                            {
                                testContext.WriteLine("Downloading data source: '{0}' to '{1}'",
                                                      dataSource.DataSourceBaseUri, dataSource.DataSourcePath);

                                Helper.DownloadFile(dataSource.DataSourceBaseUri, dataSource.DataSourcePath);
                            }
                        }
                    }
                    else
                    {
                        testContext.WriteLine("There are no configured data sources to download.");
                    }
                }
                else
                {
                    // Provide some minimal feedback that the config file could not be located
                    testContext.WriteLine(
                        "The configuration '{0}' could not be loaded.  Please check the path to ensure the file exists.",
                        configPath);
                }
            }
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the output file.
        /// </summary>
        /// <value>The output file.</value>
        public string OutputFile { get; private set; }

        #endregion

        #region Internal Properties

        /// <summary>
        /// Gets the test class.
        /// </summary>
        /// <value>The test class.</value>
        internal string TestClass { get; private set; }

        /// <summary>
        /// Gets the functions.
        /// </summary>
        /// <value>The functions.</value>
        internal IList<TestMethodHelper> Functions { get; private set; }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            MultiAssertTestXmlRepository.Persist(this, OutputFile);
        }

        #endregion

        #region Utility Methods

        /// <summary>
        /// Gets a collection of data source information.
        /// </summary>
        /// <param name="appConfigPath">The application configuration file to be parsed.</param>
        /// <returns>A list of <see cref="DataSourceInfo"/> containing information for each
        /// data source found in the configuration file.</returns>
        public List<DataSourceInfo> GetDataSources(string appConfigPath)
        {
            var dataSources = new List<DataSourceInfo>();
            DataSourceInfo dsInfo;
            XmlDocument doc;
            XmlNode dsNameNode;
            XmlNode dsBaseUriNode;

            if (File.Exists(appConfigPath))
            {
                doc = new XmlDocument();
                doc.Load(appConfigPath);

                var dataSourceNodes = doc.SelectNodes("/configuration/microsoft.visualstudio.testtools/dataSources/add");

                if (dataSourceNodes != null)
                {
                    foreach (XmlNode dsNode in dataSourceNodes)
                    {
                        dsNameNode = doc.SelectSingleNode(
                            string.Format("/configuration/connectionStrings/add[@name='{0}']",
                                          dsNode.Attributes.GetNamedItem("connectionString").Value));

                        dsInfo = new DataSourceInfo
                                     {
                                         Name = Helper.GetAttributeValue(dsNode.Attributes, "name"),
                                         DataSourcePath =
                                             Helper.GetConnectionStringSetting(
                                             Helper.GetAttributeValue(dsNameNode.Attributes, "connectionString"),
                                             ConnectionStringSetting.DataSource),
                                         ConnectionStringName =
                                             Helper.GetAttributeValue(dsNode.Attributes, "connectionString"),
                                         DataTableName = Helper.GetAttributeValue(dsNode.Attributes, "dataTableName"),
                                         DataAccessMethod =
                                             Helper.GetAttributeValue(dsNode.Attributes, "dataAccessMethod")
                                     };

                        dsBaseUriNode = doc.SelectSingleNode(
                            string.Format(
                                "/configuration/appSettings/add[@key='{0}']",
                                "DDTCB:" + dsInfo.ConnectionStringName));

                        if (dsBaseUriNode != null)
                        {
                            dsInfo.DataSourceBaseUri = Helper.GetAttributeValue(dsBaseUriNode.Attributes, "value");
                        }

                        dataSources.Add(dsInfo);
                    }
                }
            }

            return dataSources;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Gets a new instance of <see cref="TestMethodHelper"/>.
        /// </summary>
        /// <param name="testContext">A <see cref="TestContext"/> instance used to provide
        /// information about the unit tests.</param>
        /// <returns>An instance of <see cref="TestMethodHelper"/>.</returns>
        /// <remarks>By default, all outputs with a suffix of "??" will be added to the
        /// results list.  Use an overridden version of this function to suppress
        /// this behavior.</remarks>
        public TestMethodHelper GetFunctionHelper(TestContext testContext)
        {
            return GetFunctionHelper(testContext, "??");
        }

        /// <summary>
        /// Gets a new instance of <see cref="TestMethodHelper"/>.
        /// </summary>
        /// <param name="testContext">A <see cref="TestContext"/> instance used to provide
        /// information about the unit tests.</param>
        /// <param name="outputsFilterSuffix">If non-null, specifies a suffix filter for adding
        /// outputs to the results list.  If null, no outputs will be added.</param>
        /// <returns>An instance of <see cref="TestMethodHelper"/>.</returns>
        public TestMethodHelper GetFunctionHelper(TestContext testContext, string outputsFilterSuffix)
        {
            // create a new data item to hold the results.
            var resultFunction = new TestMethodHelper(testContext);

            Functions.Add(resultFunction);

            if (!string.IsNullOrEmpty(outputsFilterSuffix))
            {
                resultFunction.AddOutputs(outputsFilterSuffix);
            }

            return resultFunction;
        }

        #endregion
    }
}